作业回顾:全局异常过滤器
上节布置的作业是实现一个捕获所有异常的全局过滤器。很多同学发现,在 main.ts 中注册多个全局过滤器时,只有第一个会执行——这是因为全局过滤器中,@Catch() 无参的兜底过滤器只能注册一个。
实现效果
完成后的全局异常过滤器会捕获以下信息:
| 信息 | 来源 |
|---|---|
| 请求 Headers | request.headers |
| 请求 Body | request.body |
| 路径参数(Params) | request.params |
| 查询参数(Query) | request.query |
| 请求时间 | new Date().toISOString() |
| 客户端 IP | request-ip 库 |
| 错误信息 | exception.message |
| 异常堆栈 | exception.stack |
同时在 Winston 日志文件中记录完整的请求上下文和错误详情。
关键知识点
1. @Catch() 无参捕获所有异常
@Catch() // 无参数 = 捕获所有类型的异常
export class AllExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
// 不仅捕获 HttpException
// 还捕获 WebSocket 异常、运行时错误等
}
}
typescript
不传参数的 @Catch() 会捕获所有类型的异常,包括:
HttpException及其子类(NotFoundException、ForbiddenException 等)- 非 HTTP 异常(如 TypeError、ReferenceError)
- WebSocket / 微服务异常
2. request-ip 获取客户端真实 IP
pnpm add request-ip
pnpm add -D @types/request-ip
bash
import * as requestIp from 'request-ip';
const ip = requestIp.getClientIp(request) || 'unknown';
typescript
request-ip 会按优先级从以下 Header 中解析真实 IP:
X-Client-IPX-Forwarded-For(第一个地址)X-Real-IPCF-Connecting-IP(Cloudflare)True-Client-IP- 连接的远程地址(
request.connection.remoteAddress)
3. undefined 字段自动过滤
在响应体中,如果某个字段的值是 undefined,JSON 序列化时会自动忽略:
const responseBody = {
code: 404,
timestamp: new Date().toISOString(),
path: request.url,
headers: request.headers, // 有值 → 包含在响应中
body: request.body, // undefined → 自动忽略
query: request.query, // 有值 → 包含在响应中
ip: requestIp.getClientIp(request),
message: exception.message,
};
// JSON.stringify 会自动跳过 undefined 的字段
typescript
4. 完整的过滤器实现
// common/filters/all-exception.filter.ts
import {
Catch, ExceptionFilter, ArgumentsHost,
HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import * as requestIp from 'request-ip';
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionFilter.name);
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = {
code: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(request),
headers: request.headers,
body: request.body,
params: request.params,
query: request.query,
ip: requestIp.getClientIp(request) || 'unknown',
message: exception instanceof Error ? exception.message : 'Internal server error',
};
this.logger.error(
`${responseBody.path} → ${httpStatus} | IP: ${responseBody.ip}`,
exception instanceof Error ? exception.stack : undefined,
);
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
typescript
在 main.ts 中注册
// main.ts
import { HttpAdapterHost } from '@nestjs/core';
import { AllExceptionFilter } from './common/filters/all-exception.filter';
const app = await NestFactory.create(AppModule);
const httpAdapterHost = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionFilter(httpAdapterHost));
typescript
全局只能注册一个无参
@Catch()的兜底过滤器。如需同时注册特定类型过滤器(如@Catch(HttpException)),可以注册多个,NestJS 会按类型匹配。
↑